<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>理解原型</title>
</head>

<body>
    <script>
        window.onload = function () {
            // 理解原型
            (function () {
                // 当创建构造函数之后就为这个函数添加了prototype 原型属性 这个属性默认只会取得constructor属性，
                // 其他是继承Object对象而来
                // 而这个prototype.constructor指向这个prototype所在函数的指针
                // 也就是 OP.prototype.constructor = OP 
                var OP = function (name) {
                    this.name = name;
                }
                console.log(OP.prototype.constructor === OP); //true    

                // 当调用构造函数创建了一个新实例时，该实例的内部就会自动添加一个指针[[Prototype]]/__proto__
                // 这个指针指向该实例构造函数的原型对象
                // 即 p__proto__=OP.prototype

                // 注意：这个连接是实例与构造函数的原型对象之间，而不是存在于实例与构造函数之间
                // 换句话说 实例与构造函数之间没有直接的关系
                var p = new OP("whmk");
                console.log(p.__proto__ === OP.prototype); //true

                // 使用Object.getPrototypeOf(实例) 可以取得实例的__proto__ 对象
                // 可以很方便的取得一个对象的原型，而这在利用原型实现继承的情况下是非常重要的
                console.log(Object.getPrototypeOf(p) === OP.prototype); //true
                console.log(Object.getPrototypeOf(p).constructor === OP); //true

                // 可以在OP.prototype 上继续添加属性和方法 在所有实例中都可以共享它的属性和方法
                OP.prototype.age = 22;
                OP.prototype.sayName = function () {
                    console.log(this.name);
                }

                p.sayName(); //whmk;
            })();

            // 原型的动态性 不可轻易重写原型
            (function () {

                function Person() {
                    this.name = 'whmk'
                }
                // 先创建了实例
                var f = new Person();

                // 先创建了实例 再重写了Person的原型，便切断了实例与构造函数原型对象的连接
                // 即实例引用的还是最初的原型，最初的原型里没有如下属性和方法，
                // 所以在实例中调用重写原型后的属性方法就会发生错误 
                Person.prototype = {
                    constructor: Person,
                    name: 'Nico',
                    age: 22,
                    sayName: function () {
                        console.log(this.name);
                    }
                }

                // f.sayName(); //报错，sayName is not a function
            })();

            // 组合使用构造函数模式和原型模式 创建自定义类型最常见方式
            // 也就是该共享提出来交给原型，该私有的提出来交给构造函数，创建实例后 互不相干
            (function () {
                // 构造函数定义实例的属性
                function Person(name, age, job) {
                    this.name = name;
                    this.age = age;
                    this.job = job;
                    this.friends = ["sda", "dsaa"];
                }


                // 原型模式用于定义方法和共享的属性
                // 先重写了原型模式，再创建实例，便可以继续共享原型模式的属性和方法，
                // 因为重写的缘故，因此constructor也必须强制指向原先的构造函数
                Person.prototype = {
                    constructor: Person,
                    sayName: function () {
                        return this.name;
                    }
                }

                // 每个实例都会有一份实例属性的副本，同时又共享着对方法的引用
                var p1 = new Person('whmkl', 22, 'daewa');
                var p2 = new Person("JJJ", 23, "xcssdad");


                // 每个实例都会有一份实例属性的副本,所以在p1中添加的friends不会影响到p2
                p1.friends.push("p1p1");
                console.log(p1.friends); //["sda","dsaa","p1p1"] 
                console.log(p2.friends); //["sda","dsaa"]

                // 各自不同的实例
                console.log(p2.friends === p1.friends); //false

                // 共享的方法
                console.log(p2.sayName === p1.sayName); //true

                // 往后可以继续添加一个或多个共享的方法或属性
                Person.prototype.sayAge = function () {
                    return this.age;
                };

                console.log(p2.sayAge()); // 22
                console.log(p2.sayName()); //JJJ
            })();

            // 理解继承 由于函数没有签名，在ES5中无法实现接口继承，只支持实现继承（对实际方法的继承），
            // 实现继承注意依靠原型链实现
            (function () {
                // 继承的基本思想就是一个引用类型继承另一个引用类型的属性或方法
                // 那么为什么依靠原型链可以实现继承？
                // 当创建构造函数时会为构造函数添加一个prototype原型属性（对象），而这个原型中有一个constructor 
                // 指向引用prototype函数的指针
                // 当调用构造函数创建实例时会为这个实例添加一个__proto__的指针指向构造函数的原型对象即（prototype）;
                // 那么这个实例就会共享着原型对象中的属性和方法
                // 那么如果将这个原型对象等于另一个类型（构造函数）的实例，
                // 那么这个原型对象就共享着另一个类型的原型对象的属性和方法
                // 再接着如果将另一个类型的原型对象等于另另一个类型（构造函数）的实例...以此类推就构成了原型链

                // 定义父类构造函数 
                function Parent() {
                    this.friends = ["Parent1", "Parent2"];
                }

                // 在父类的原型上添加一个方法
                Parent.prototype.sayParent = function () {
                    console.log("Parent");
                }

                // 定义子类的构造函数
                function Child(name, age) {
                    this.name = name;
                    this.age = age;
                }

                // 关键一步  将子类的原型对象作为父类的实例 即子类的原型对象上
                // 就共享着另一个类型的原型对象的属性和方法，并且拥有父类的构造函数的实例属性
                // 这里继承的主要方法是重写了子类的原型对象，
                // 因此必须重写了原型对象后才能创建新实例（或为原型对象添加新的属性方法）
                // 否则将不能使用重写原型对象后的属性和方法
                // 再者 这里Child.prototype.constructor 因为重写继承的缘故，
                // Child.prototype.constrcutor指向Parent
                // 所以c1.__proto__.constructor=Parent
                Child.prototype = new Parent();
                c1 = new Child('c1c1c1', 22);
                console.log(Child.prototype.constructor === Parent); //true
                console.log(c1.__proto__.constructor === Parent); //true 
                console.log(c1.constructor === Parent); //true 
                // 因此这样一来不能用constructor来确认实例到底是那个构造函数的实例了


                // 因为Child.prototype 继承了父类的构造实例属性 并 共享父类原型对象上的属性和方法
                // 所以子类可以在 Child.prototype 上找到这个friends属性 
                console.log(c1.friends); //["Parent1","Parent2"]

                // 给子类型的原型添加属性方法一定要在子类型的原型继承了父类时才可以添加
                // 可以为子类的原型继续添加供实例共享的属性和方法
                Child.prototype.sayChild = function () {
                    console.log("Child")
                }

                // 可以为父类的原型继续添加供子类原型（实例）共享的属性和方法
                Parent.prototype.sayOK = function () {
                    console.log("Parent OK");
                }

                // 在实例的__proto__即子类的原型上(prototype)上找到
                c1.sayChild();

                //在实例的__proto__即子类的原型上(prototype)上的__proto__即（父类的原型上prototype）上找到 
                c1.sayOK();
                console.log(c1)

                // 根据原型链继承 一个构造函数的原型作为另一个构造函数实例 ，
                // 作为实例对象必有一个__proto__指针指向另一个构造函数的原型（prototype）
                // 因为所有引用类型都继承了Object 
                // 所以所有函数的默认原型(重写原型不算)都是Object的实例，
                // 因此默认原型内部都会包含一个__proto__指针指向Object.prototype
                console.log(Parent.prototype.__proto__ === Object.prototype); //true
            })();

            // 组合继承 利用子类型调用超类型的构造函数重写超类型的属性 就可以避免引用类型在所有实例中的共享问题
            (function () {


                function Parent(name) {
                    this.name = name;
                    this.colors = ['red', 'blue'];
                }

                Parent.prototype.sayName = function () {
                    console.log(this.name);
                }

                function Child(name, age) {
                    // 关键代码 
                    // 利用子类型调用超类型的构造函数重写超类型的属性
                    // 因此在创建实例时就获取了超类型所有的属性
                    // 就可以避免引用类型（数组、对象等）在所有实例中的共享问题
                    Parent.call(this, name);
                    this.age = age;
                }

                // 重写原型
                // 在Child.prototype 中也有一份同样的超类型属性，且共享超类型原型的方法
                Child.prototype = new Parent();
                // 重写原型时丢失了constructor的默认指向,所以强制让他指会默认的Child,方便理解，和利用constructor判断等
                Child.prototype.constructor = Child;

                // 为子类型原型添加新的方法 但必须在Child.prototype=new Parent();执行之后
                Child.prototype.sayAge = function () {
                    console.log(this.age);
                }

                // 在创建实例时就同时获取了子类型的实例属性和超类型所有的属性
                // 同时在Child.prototype 中也有一份同样的超类型属性
                var c1 = new Child('whmk', 22);
                var p1 = new Parent("parent");
                console.log(c1);
                console.log(p1);

                c1.colors.push("black"); //实例中有了私有的colors属性 ，就不会影响其他的实例了
                console.log(c1.colors); //['red','blue',"black"]
                console.log(p1.colors); //['red','blue']
            })();

            // 使用寄生组合式继承 
            // 不必为了指定子类型的原型而调用超类型的构造函数，我们所需要的不就是超类型的实例属性和原型副本吗
            // 那么使用了 Parent.call(this, name); 方法后我们就不需要在显示的调用  Child.prototype = new Parent();
            // 这样就会在实例和子类型的原型上都有超类型的实例属性副本，而我们现在就只需要超类型的原型副本
            // 所以现在只需要继承超类型的原型，然后再将结果指定给子类型的原型就可以了
            (function () {
                function changeProto(child, parent) {
                    // 第一步创建超类型原型的副本
                    var tempPrototype = Object.create(parent);
                    // 增强对象 将因为重写子类原型丢失的默认constructor 强制转换回来
                    tempPrototype.constructor = child;
                    // 最后再将新创建的原型副本赋值给子类型的原型
                    child.prototype = tempPrototype
                }

                function Parent(name) {
                    this.name = name;
                    this.colors = ['red', 'blue'];
                }

                Parent.prototype.sayName = function () {
                    console.log(this.name);
                }

                function Child(name, age) {
                    // 关键代码 
                    // 利用子类型调用超类型的构造函数重写超类型的属性
                    // 因此在创建实例时就获取了超类型所有的属性
                    // 就可以避免引用类型（数组、对象等）在所有实例中的共享问题
                    Parent.call(this, name);
                    this.age = age;
                }
                
                // 继承超类型的原型 而不是调用超类型的构造函数了
                // 这样就不会在实例和子类型的原型上都有超类型的实例属性副本，
                changeProto(Child, Parent);

                // 为子类型原型添加新的方法 但必须在Child.prototype=new Parent();执行之后
                Child.prototype.sayAge = function () {
                    console.log(this.age);
                }

                // 在创建实例时就同时获取了子类型的实例属性和超类型所有的属性
                // 同时在Child.prototype 中也有一份同样的超类型属性
                var c1 = new Child('whmk', 22);
                console.log(c1);
            })(); 
        }
    </script>
</body>

</html>